package com.example.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.example.realm.ManagerRealm;
import com.example.realm.UserRealm;
import com.example.token.MyModularRealmAuthenticator;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * Shiro 的过滤器是全局拦截器，前端每次请求一发出，就会被过滤器先拦截判断，是否允许访问 Controller 方法。
 * 1、前端请求 → ShiroFilter 判断路径规则（anon/authc/roles/perms）
 * ↓
 * 2、如果 anon 或 已认证 → 放行到 Controller
 * ↓
 * 3、Controller 处理（登录接口才会调用 subject.login(token)）
 * ↓
 * 4、subject.login() → SecurityManager → Realm → 数据库验证用户身份
 * ↓
 * 5、验证成功后 → 登录状态保存在 session 中
 * ↓
 * 6、后续请求再进 ShiroFilter（根据认证状态 & 权限判断是否放行）
 */
@Configuration
public class ShiroConfig {

    /**
     * @Bean 注解用在 方法上
     * 表示这个方法的返回值会被 Spring 注册到 IoC 容器中
     * 默认注册的 Bean 名称是 方法名
     */

    /**
     * 自定义认证器
     * 将自定义认证器交给 Spring 容器托管，便于后续注入到 SecurityManager
     */
    @Bean
    public MyModularRealmAuthenticator myModularRealmAuthenticator() {
        return new MyModularRealmAuthenticator();
    }

    /**
     * Shiro 多 Realm 配置
     */

    /**
     * 自定义UserRealm
     */
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }

    /**
     * 自定义ManagerRealm
     */
    @Bean
    public ManagerRealm managerRealm() {
        return new ManagerRealm();
    }

    /**
     * 基于Cookie实现的RememberMe管理器
     */
    @Bean
    public CookieRememberMeManager cookieRememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        /**
         * Cookie必须设置名称
         */
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        /**
         * 设置Cookie的存活时间
         */
        simpleCookie.setMaxAge(30 * 24 * 60 * 60);
        cookieRememberMeManager.setCookie(simpleCookie);
        return cookieRememberMeManager;
    }

    /**
     * 自定义 Session 管理器
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        /**
         * 输出Session默认过期时间
         */
        System.out.println("Session默认过期时间：" + sessionManager.getGlobalSessionTimeout());
        /**
         * 设置Session过期时间
         */
        sessionManager.setGlobalSessionTimeout(15 * 1000);
        return sessionManager;
    }

    /**
     * Shiro缓存使用
     * Shiro + Ehcache 的整合流程如下：
     * 1. 配置 Shiro 使用 EhCacheManager
     * 2. Realm 中开启缓存（认证信息缓存、授权信息缓存）
     * 3. 提供 ehcache.xml 缓存配置文件
     * 4. 使用时自动缓存登录用户的认证/授权信息
     */
    @Bean
    public EhCacheManager ehCacheManager() {
        EhCacheManager ehCacheManager = new EhCacheManager();
        ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return ehCacheManager;
    }

    /**
     * 1、将 Shiro 和 Thymeleaf 集成起来，使你可以在 HTML 页面中使用 <shiro:hasPermission>、<shiro:hasRole> 等标签进行权限判断和界面控制。
     * 2、ShiroDialect 是来自 thymeleaf-extras-shiro 包的扩展方言（Dialect），注册它后 Thymeleaf 才能识别 <shiro:...> 标签。
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

    /**
     * “凡是带有 @RequiresPermissions 等注解的方法，执行前都要先拦截下来，交由 Shiro 做权限判断。”
     * ↓ Controller 方法带 @RequiresPermissions
     * ↓ Spring 容器启动时加载 advisor（AuthorizationAttributeSourceAdvisor）
     * ↓ 由 advisorAutoProxyCreator 为 Controller 生成代理对象
     * ↓ 调用方法时 AOP 拦截器生效
     * ↓ 调用 Shiro -> Subject -> hasPermission()
     * ↓ 从 Realm 获取权限信息
     * ↓ 判断权限是否通过
     */

    /**
     * 启用 AOP 代理，使注解类能被拦截
     * DefaultAdvisorAutoProxyCreator：是 Spring AOP 的基础设施类，负责自动为带有 @Aspect（切面）或 @Advisor 的 Bean 生成代理对象。Shiro 的注解也是一种 Advisor
     * setProxyTargetClass(true)：使用 CGLIB 方式代理（默认 false 使用 JDK 动态代理，只能代理接口），若你的类没有实现接口，必须设为 true 否则注解不生效！
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        /**
         * 强制使用 CGLIB 动态代理
         */
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 识别 @RequiresXXX 注解，触发授权判断
     * AuthorizationAttributeSourceAdvisor：是 Shiro 提供的一个 Advisor，它会扫描所有 @RequiresRoles / @RequiresPermissions 注解的方法，构建拦截器链
     * setSecurityManager：注入当前项目的 Shiro SecurityManager，否则无法判断权限逻辑
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(defaultWebSecurityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 用户登录认证流程：加盐-->加密-->散列
     * 1.加盐
     * 2.加密
     * 3.散列
     * 用户注册与用户登录进行以上三步操作
     */

    /**
     * 配置密码加密规则
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        /**
         * matcher指定加密规则
         * 加密算法：如MD5、SHA-1、SHA-256等
         */
        matcher.setHashAlgorithmName("MD5");
        /**
         * 散列次数，增强破解难度
         * 指定散列次数：要与用户注册时散列次数一致
         */
        matcher.setHashIterations(1);
        return matcher;
    }

    /**
     * 调用 login(token) 后，Shiro 开始执行内部认证流程：
     * 1、SecurityManager.login() → 触发验证器 → 找到绑定的 Realm
     * 2、JdbcRealm 会从数据库里查出账号密码，进行比对
     * 3、如果一致，登录成功，Shiro 会将用户信息放入 session，标记为已认证状态
     * 4、否则抛出异常，进入 catch 分支
     */

    /**
     * IniRealm
     */
    @Bean
    public IniRealm getIniRealm() {
        /**
         * Realm 认证逻辑
         */
        return new IniRealm("classpath:shiro.ini");
    }

    /**
     * 手动创建 Druid 的 DataSource Bean
     */
//    @Bean
//    @ConfigurationProperties(prefix = "spring.datasource")
//    public DataSource dataSource() {
//        return new com.alibaba.druid.pool.DruidDataSource();
//    }

    /**
     * JdbcRealm
     */
    @Bean
    public JdbcRealm getJdbcRealm(DataSource dataSource) {
        /**
         * Realm 认证逻辑
         */
        JdbcRealm jdbcRealm = new JdbcRealm();
        /**
         * JdbcRealm自行从数据库查询用户、角色以及权限数据
         * 数据库表结构需要符合JdbcRealm的规范
         */
        jdbcRealm.setDataSource(dataSource);
        /**
         * JdbcRealm默认开启认证功能，需要手动开启授权功能
         */
        jdbcRealm.setPermissionsLookupEnabled(true);
        return jdbcRealm;
    }

    /**
     * 自定义Realm
     */
    @Bean
    public MyRealm myRealm(HashedCredentialsMatcher matcher) {
        MyRealm myRealm = new MyRealm();
        /**
         * 将匹配器注入到 Realm 中
         */
        myRealm.setCredentialsMatcher(matcher);
        return myRealm;
    }

    /**
     * 向 Spring 容器注册一个名为 getDefaultWebSecurityManager 的 Bean，类型为 DefaultWebSecurityManager。
     * 1、@Bean
     * public DefaultWebSecurityManager getDefaultWebSecurityManager()	           "getDefaultWebSecurityManager"（默认）
     * 2、@Bean("securityManager")                                                 "securityManager"（自定义）
     */

//    @Bean
//    public DefaultWebSecurityManager getDefaultWebSecurityManager(IniRealm iniRealm) {
//        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//        /**
//         * securityManager完成校验需要realm
//         */
//        securityManager.setRealm(iniRealm);
//        return securityManager;
//    }
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm myRealm,
                                                                  EhCacheManager ehCacheManager,
                                                                  DefaultWebSessionManager webSessionManager,
                                                                  CookieRememberMeManager cookieRememberMeManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        /**
         * 单个 Realm 配置
         * securityManager完成校验需要realm
         */
        securityManager.setRealm(myRealm);
        /**
         * 设置缓存管理器
         */
        securityManager.setCacheManager(ehCacheManager);
        /**
         * 设置Session管理器
         */
        securityManager.setSessionManager(webSessionManager);
        /**
         * 设置RememberMe管理器
         */
        securityManager.setRememberMeManager(cookieRememberMeManager);
        /**
         * 设置自定义认证器（在多Realm配置之前）
         * Shiro 多 Realm 配置：分支处理
         * 设置认证器为自定义的 MyModularRealmAuthenticator，用于分支选择 Realm
         */
//        securityManager.setAuthenticator(myModularRealmAuthenticator());
        /**
         * Shiro 多 Realm 配置：链式处理
         * 分支处理包含链式处理
         * 配置多个 Realm：
         * userRealm：普通用户认证
         * managerRealm：管理员认证
         * 此处配合自定义认证器实现了按 loginType 分支选择 Realm 认证
         */
//        Collection<Realm> realms = new ArrayList<>();
//        realms.add(userRealm());
//        realms.add(managerRealm());
//        securityManager.setRealms(realms);
        return securityManager;
    }

    /**
     * 参数注入机制原理：Spring 注入参数时，不依赖参数名匹配，而是依赖参数类型匹配
     * Spring 在处理 @Bean 方法时，会做如下步骤：
     * 判断方法是否有参数；
     * 对于每个参数，从容器中查找对应类型的 Bean（使用类型匹配）；
     * 找到后注入调用该方法；
     * 将方法的返回值注册为新的 Bean。
     * 所以只要你在容器中有这个类型的 Bean，Spring 就能注入进去。
     */

    /**
     * Shiro 的过滤器链
     * 组装 Shiro 安全系统的核心部件，生成并注册最终的权限过滤器
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
        /**
         * 设置shiro拦截规则
         * anno     匿名用户可访问
         * authc    认证用户可访问
         * user     使用RememberMe用户可访问
         * perms    对应权限可访问
         * roles     对应角色可访问
         */
        Map<String, String> filterMap = new HashMap<>();
        /**
         * 基于过滤器的权限控制
         */
        filterMap.put("/", "anon");
        filterMap.put("/index.html", "user");
        filterMap.put("/login.html", "anon");
        filterMap.put("/register.html", "anon");
        filterMap.put("/user/login", "anon");
        filterMap.put("/user/register", "anon");
        filterMap.put("/static/**", "anon");
        filterMap.put("/**", "authc");

        filterMap.put("/c_add.html", "perms[sys:c:save]");
//        filterMap.put("/customer/list", "perms[sys:k:find]");

        /**
         * 在Shiro过滤器中进行配置，配置logout对应的路径
         * 在页面“退出”按钮上，跳转到logout对应的url
         */
        filterMap.put("/logout", "logout");
        filter.setFilterChainDefinitionMap(filterMap);
        /**
         * 未登录自动跳转
         */
        filter.setLoginUrl("/login.html");
        /**
         * 只有登录成功以后才会进行权限管理
         * 无权限跳转该页面
         */
        filter.setUnauthorizedUrl("/lessPermission.html");
        /**
         * 过滤器是shiro进行权限校验的核心
         * 进行认证与授权需要SecurityManager
         */
        filter.setSecurityManager(defaultWebSecurityManager);
        return filter;
    }

}
/**
 * | 错误/易错点                      | 原因                                            | 建议
 * | 方法没有 `@Bean` 注解            | Spring 不会识别注册                              | 必须加上 `@Bean`
 * | 忘记加 `@Configuration` 类声明   | 容器不会处理 `@Bean` 方法                         | 容器类要加 `@Configuration`
 * | 同类型 Bean 存在多个             | 自动注入失败（`NoUniqueBeanDefinitionException`） | 加 `@Qualifier("beanName")`
 * | 方法名不是标准命名                | 注册名难以管理                                   | 建议命名清晰或显式命名
 * | 在非 Spring 管理类中写 `@Bean`   | 不生效                                          | 必须写在 `@Configuration` 类中
 * <p>
 * | 是否注册 | 加 `@Bean` 就是注册 Bean
 * | 默认名称 | 方法名作为 Bean 名称
 * | 改名方式 | `@Bean("xxx")` 指定名称
 * | 必要前提 | 方法所在类要有 `@Configuration` 或 `@Component`
 * | 注入方式 | `@Autowired`、`@Resource`、`@Qualifier`
 */